// Copyright (c) 2021 Presto Labs Pte. Ltd.
// Author: hgpark

#pragma once

#include <glog/logging.h>
#include <spdlog/spdlog.h>

#include <algorithm>
#include <cmath>
#include <functional>
#include <map>
#include <memory>
#include <set>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>

#include "coin/proto/coin_order_enums.pb.h"
#include "coin2/app/driver.h"
#include "coin2/base/log.h"
#include "coin2/exchange/base/feed/update.h"
#include "coin2/strategy/order_executor/executor.h"
#include "coin2/exchange/risk/risk_manager.h"
#include "coin2/strategy/rmm_strat/rmm_strat_config.pb.h"
#include "coin2/strategy/rmm_strat/rmm_pricer_window.h"
#include "coin2/strategy/support/csv_writer.h"
#include "coin2/strategy/strategy.h"
#include "coin2/strategy/util/moving_ohlc.h"
#include "coin2/strategy/util/config_reporter.h"
#include "presto/quant/base/time.h"
#include "presto/quant/math/moving_average.h"
#include "presto/quant/math/ewma.h"
#include "presto/quant/math/moving_window.h"

namespace coin2::strategy::rmm_strat {

using ::coin::proto::OrderDirection;
using ::coin::proto::OrderEvent;
using ::coin::proto::OrderState;
using ::coin::proto::TradeSide;
using ::coin2::app::impl::Driver;
using ::coin2::exchange::base::feed::FeedUpdate;
using ::coin2::exchange::base::market::ExchangeType;
using ::coin2::exchange::base::market::MarketExchangeApi;
using ::coin2::exchange::base::order::IOrderSubsystem;
using ::coin2::exchange::base::order::OrderUpdate;
using ::coin2::exchange::base::order::AccountInfo;
using ::coin2::exchange::base::symbology::IProductHolder;
using ::coin2::exchange::base::order::IOrderContext;
using ::coin2::exchange::base::strategy_util::PrometheusHealthReporter;
using ::coin2::exchange::order::RiskManagerConfig;
using ::coin2::exchange::risk::RiskManager;
using ::coin2::strategy::order_executor::OrderExecutorSystem;
using ::coin2::strategy::util::InternalControlRequest;
using ::coin2::strategy::util::GetConfigResponse;
using ::coin2::strategy::util::ConfigType;
using ::coin2::strategy::util::ConfigReporter;
using ::coin2::strategy::support::CsvWriter;


struct OrderContextVector {
  OrderContextVector() {
    oc_vector.reserve(8);
  }
  std::vector<const IOrderContext*> oc_vector;
  void push_back(const IOrderContext* oc) {
    oc_vector.push_back(oc);
  }
};

class QtyRandomizer {
public:
  QtyRandomizer() : rd{}, mt{rd()}, dist{0.999, 1.0} {} 
  double rand() {
    double val = dist(mt);
    return val;
  }
private:
  std::random_device rd;
  std::mt19937 mt;
  std::uniform_real_distribution<> dist;
};

enum class AccountType {
  RMM_STRAT_0 = 0,
  RMM_STRAT_1,
  HEDGE,
  UNKNOWN
};

class RmmPricer;
class RmmSubStrategy;

class RmmStrategy : public ::coin2::strategy::IStrategy {
 public:
  RmmStrategy(const std::experimental::filesystem::path& strat_json, int32_t strat_num,
              bool is_sim, const std::experimental::filesystem::path& sim_result_path, int64_t sim_start_ts);
  std::string Owner() { return owner_; }
  std::string HedgeOwner() { return hedge_owner_; }
  std::string SystemName() { return system_name_; }
  std::string HedgeSystemName() { return hedge_system_name_; }
  std::string StratName() { return strat_name_; }
  std::string HedgeStratName() { return hedge_strat_name_; }
  int64_t OtherStratTimestamp() { return other_strat_ts_; }
  void Init(::coin2::app::Driver* driver) override;
  void InitOwner(::coin2::app::Driver* driver);
  void InitStrategyReporter2();
  void InitReserveMap();
  bool FeedUpToDate(const FeedUpdate& upd) const;
  bool IsPriceOnlySystemName(const std::string& system_name);
  bool IsSnapShotOnlySystemName(const std::string& system_name);
  void onBookFeed(const FeedUpdate& upd) override;
  void onTradeFeed(const FeedUpdate& upd) override;
  void onIndexFeed(const FeedUpdate& upd) override;
  void onAccountInfo(const OrderUpdate& upd) override;
  void onAccountOrder(const OrderUpdate& upd) override;
  void onOrderLog(const coin::proto::OrderGatewayLog& order_log) override;
  void onOrderSubsystemError(const OrderUpdate&) override;
  void onCleanup(Driver* driver) override;
  void onInternalControlRequest(const InternalControlRequest&) override;
  void SetMainIoc(boost::asio::io_context* ioc) { main_ioc_ = ioc; }

 private:
  bool is_sim_{false};
  int64_t strategy_created_time_{0L};
  int64_t strat_num_{0L};
  int64_t other_strat_ts_{0L};
  int64_t last_strat_submit_report_time_{0L};
  bool enable_hedge_;
  std::string owner_;
  std::string hedge_owner_;
  std::string system_name_;
  std::string hedge_system_name_;
  std::string strat_name_;
  std::string hedge_strat_name_;
  std::unique_ptr<RmmSubStrategy> rmm_sub_strat_;
  std::unordered_map<std::string, std::vector<RmmSubStrategy*>> feed_sub_map_;
  std::unordered_map<std::string, RmmSubStrategy*> order_sub_map_;
  std::vector<std::string> symbol_list_for_config_service_;
  std::unique_ptr<RiskManager> risk_manager_;
  std::unique_ptr<ConfigReporter> config_reporter_;
  std::unique_ptr<StrategyReporter2> strat_reporter_;
  std::unique_ptr<StrategyReporter2> hedge_strat_reporter_;
  boost::asio::io_context* main_ioc_{nullptr};
};

inline double GetQtyFromQuote(
    const coin2::exchange::base::symbology::IProductHolder& pholder,
    double price,
    double quote) {
  const auto& pi = pholder.product_info();
  double contract_value = 1.0;
  if (pi.contract_value() > 0) {
    contract_value = pi.contract_value();
  }
  if (pi.is_inverse()) {
    return quote / contract_value;
  } else {
    return quote / price / contract_value;
  }
}

inline double GetQuoteFromQty(
    const coin2::exchange::base::symbology::IProductHolder& pholder,
    double price,
    double qty) {
  const auto& pi = pholder.product_info();
  double contract_value = 1.0;
  if (pi.contract_value() > 0) {
    contract_value = pi.contract_value();
  }
  if (pi.is_inverse()) {
    return qty * contract_value;
  } else {
    return qty * price * contract_value;
  }
}

struct RmmConfigState {
  RmmConfigState(RmmStrategy* parent, const RmmProductConfig& product_config, int32_t strat_num);
  void UpdateControlConfig(const std::string& config_ctrl_pb_str);

  RmmStrategy* parent_{nullptr};
  RmmProductConfig config_const_;
  std::unique_ptr<IProductHolder> trade_product_holder_;
  std::unique_ptr<IProductHolder> hedge_product_holder_;
  OrderExecutorSystem* oe_ = nullptr;
  OrderExecutorSystem* hedge_oe_ = nullptr;
  std::optional<RmmProductConfig> config_;
  RiskManager* risk_manager_;
  double order_quote_ub = 0;
  double order_qty_ub = 0;
  double hedge_exception_quote = 0;
  double hedge_exception_qty = 0;

  void ManageQuoteSize(double midp, int64_t timestamp) {
    if (last_manage_quotesize_ts + 600e9 < timestamp) {
      const auto& prod_conf_quote = oe_->GetConfig(*trade_product_holder_);
      auto* prod_conf_orig_mutable = oe_->GetMutableOrigConfig(*trade_product_holder_);
      if (config_const_.pos_by_quote() &&
          prod_conf_quote.qty().has_max_position_quote() && prod_conf_quote.qty().has_min_position_quote()) {
        double max_pos = GetQtyFromQuote(*trade_product_holder_, midp, prod_conf_quote.qty().max_position_quote());
        double min_pos = GetQtyFromQuote(*trade_product_holder_, midp, prod_conf_quote.qty().min_position_quote());
        prod_conf_orig_mutable->mutable_qty()->set_max_position(max_pos);
        prod_conf_orig_mutable->mutable_qty()->set_min_position(min_pos);
      } else if (!config_const_.pos_by_quote() &&
          prod_conf_quote.qty().has_max_position() && prod_conf_quote.qty().has_min_position()) {
        double max_pos_quote = GetQuoteFromQty(*trade_product_holder_, midp, prod_conf_quote.qty().max_position());
        double min_pos_quote = GetQuoteFromQty(*trade_product_holder_, midp, prod_conf_quote.qty().min_position());
        prod_conf_orig_mutable->mutable_qty()->set_max_position_quote(max_pos_quote);
        prod_conf_orig_mutable->mutable_qty()->set_min_position_quote(min_pos_quote);
      } else {
        CHECK(false) << prod_conf_quote.DebugString() << "\nmax_position or max_position_quote should be set!";
      }
      double min_order_qty =
        config_const_.oe_config().common().qty().has_min_order_quote() ?
        GetQtyFromQuote(*trade_product_holder_, midp, config_const_.oe_config().common().qty().min_order_quote()) :
        prod_conf_quote.qty().min_order_qty();

      if (config_const_.bound_by_quote() && config_const_.has_order_quote_ub()) {
        order_quote_ub = config_const_.order_quote_ub();
        order_qty_ub = GetQtyFromQuote(*trade_product_holder_, midp, order_quote_ub);
      } else if (!config_const_.bound_by_quote() && config_const_.has_order_qty_ub()) {
        order_qty_ub = config_const_.order_qty_ub();
        order_quote_ub = GetQuoteFromQty(*trade_product_holder_, midp, order_qty_ub);
      } else {
        CHECK(false) << config_const_.DebugString() << "\norder_quote_ub or order_qty_ub should be set!";
      }

      if (config_const_.has_hedge_exception_quote()) {
        hedge_exception_quote = config_const_.hedge_exception_quote();
        hedge_exception_qty = GetQtyFromQuote(*trade_product_holder_, midp, hedge_exception_quote);
      }

      config_const_.mutable_oe_config()->mutable_common()->mutable_qty()->set_min_order_qty(
          min_order_qty);
      last_manage_quotesize_ts = timestamp;
      quote_pos_updated = true;
      SPDLOG_INFO("Executor Config: {}", oe_->GetConfig(*trade_product_holder_).ShortDebugString());
    }
  }

  // quotesizer
  int64_t last_manage_quotesize_ts = 0L;
  bool quote_pos_updated = false;
};

class RmmSubStrategy
    : public ::coin2::strategy::IStrategy,
      public RmmConfigState {
 public:
  RmmSubStrategy(
      RmmStrategy* parent,
      const RmmProductConfig& config,
      int64_t strategy_created_time,
      int32_t strat_num,
      bool is_sim,
      const std::experimental::filesystem::path& sim_result_path,
      int64_t sim_start_ts);
  void Init(::coin2::app::Driver* driver,
            ::coin2::strategy::util::StrategyReporter2* reporter,
            ::coin2::strategy::util::StrategyReporter2* hedge_reporter);
  void SetRiskManager(RiskManager* risk_manager) { risk_manager_ = risk_manager; }
  void SetHedgePosition(double position, int64_t timestamp) { hedge_position_ = { position, timestamp }; }
  void onBookFeed(const std::string& product_key, const FeedUpdate& upd);
  void onSnapshotBookFeed(const std::string& product_key, const FeedUpdate& upd);
  void onTradeFeed(const std::string& product_key, const FeedUpdate& upd);
  void onIndexFeed(const std::string& product_key, const FeedUpdate& upd);
  void onAccountInfo(const OrderUpdate& upd) final;
  void onAccountOrder(const OrderUpdate& upd) final;
  void FireMakerOrders(int64_t order_id, double price, OrderDirection direction, int32_t sign, int64_t timestamp);
  bool CalculateFairMidp(int64_t timestamp, bool enable_logging);
  bool AggregateRefOrders(int64_t timestamp);
  bool IsPriceOnlySystemName(const std::string& system_name);
  bool IsSnapShotOnlySystemName(const std::string& system_name);
  void UpdateWorkingOrders(int64_t timestamp);
  std::optional<double> GetPosition(AccountType account_type, int64_t curr_ts);
  std::optional<double> GetPositionForLeaning(int64_t curr_ts);
  AccountType GetAccountType(const AccountInfo& account_info);
  double LeanByPosition(double position);
  void UpdateMidp(int64_t timestamp, double midp) {
    if (fair_ask0_ != 0 && fair_bid0_ != 0) {
      // subtrack tick_size, so that |misprice_bp| reduces
      double misprice_bp = 0;
      if (fair_ask0_ < midp) {
        misprice_bp = (midp / fair_ask0_ - 1) * 1e4;
        misprice_bp = std::max(
            0., misprice_bp - (tick_size_ / fair_ask0_) * 1e4);
      } else if (fair_bid0_ > midp) {
        misprice_bp = (midp / fair_bid0_ - 1) * 1e4;
        misprice_bp = std::min(
            0., misprice_bp + (tick_size_ / fair_bid0_) * 1e4);
      }
      mispricebp_window_.Update(timestamp, misprice_bp);
    }
  }
  void Hedge(OrderDirection dir, double hedge_amount, int64_t timestamp);
  double tick_size() { return tick_size_; }
  double tick_qty() { return tick_qty_; }
  const RmmProductConfig& Config() const { return *config_; }
  const RmmProductConfig& ConfigConst() const { return config_const_; }
  const IProduct* TradeProduct() const {return trade_product_.get(); }
  const IProduct* HedgeProduct() const {return hedge_product_.get(); }
  const IProductHolder* TradeProductHolder() const { return trade_product_holder_.get(); }
  const IProductHolder* HedgeProductHolder() const { return hedge_product_holder_.get(); }
  MarketExchangeApi Mea() {
    return MarketExchangeApi::FromString(config_const_.trade_product().mea());
  }
  MarketExchangeApi HedgeMea() {
    return MarketExchangeApi::FromString(config_const_.hedge_product().mea());
  }
  IOrderSubsystem* os() { return driver_->order()->GetUniqueSystemByName(parent_->SystemName()); }
  IOrderSubsystem* hedge_os() { return driver_->order()->GetUniqueSystemByName(parent_->HedgeSystemName()); }
  OrderExecutorSystem* OrderExecutor() { return oe_; }
  OrderExecutorSystem* HedgeOrderExecutor() { return hedge_oe_; }
  void WritePriceToCsv(int64_t timestamp, std::string mea, double price, bool is_midp);

 private:
  bool enable_telemetry_{false};
  bool maker_fired_{false};
  bool is_sim_{false};
  bool outside_only_{false};
  int64_t strategy_created_time_{0L};
  int64_t strat_num_{0L};
  int64_t book_ts_{0L};
  int64_t index_ts_{0L};
  int64_t last_update_ts_{0L};
  int64_t last_hedge_ts_{0L};
  int64_t sim_start_ts_{0L};
  int64_t cancel_cnt_{0L};
  int64_t submit_cnt_{0L};
  int64_t num_order_{0L};
  double tick_size_{0};
  double tick_qty_{0};
  double fair_midp_{0};
  double fair_ask0_{0};
  double fair_bid0_{0};
  QtyRandomizer qty_randomizer;
  std::vector<const IOrderContext*> ask_ocs_;
  std::vector<const IOrderContext*> bid_ocs_;
  std::optional<double> hedge_book_ask0_;
  std::optional<double> hedge_book_bid0_;
  double num_sample_{0};
  std::optional<std::pair<double, int64_t>> strat0_position_;
  std::optional<std::pair<double, int64_t>> strat1_position_;
  std::optional<std::pair<double, int64_t>> hedge_position_;
  Driver* driver_;
  exchange::base::strategy_util::StrategyLogger* strat_logger_;
  exchange::base::strategy_util::PrometheusHealthReporter* health_reporter_;
  std::set<std::string> feed_keys_;
  std::unique_ptr<IProduct> trade_product_;
  std::unique_ptr<IProduct> hedge_product_;
  std::vector<std::unique_ptr<IProduct>> ref_products_;
  std::vector<std::unique_ptr<RmmPricer>> pricers_;
  std::map<int64_t, double> aggregated_ask_orders_;
  std::map<int64_t, double, std::greater<int64_t> > aggregated_bid_orders_;
  std::unordered_map<int64_t, OrderContextVector> working_ask_orders_;
  std::unordered_map<int64_t, OrderContextVector> working_bid_orders_;
  std::unordered_set<MarketExchangeApi> price_only_meas_;
  presto::math::MovingAverageWindow<int64_t> ref_downtime_window_;
  presto::math::MovingAverageWindow<double> spread_window_;
  presto::math::MovingAverageWindow<double> mispricebp_window_{60'000'000'000L};
  presto::math::MovingAverageWindow<double> rmm_position_window_;
  presto::math::MovingAverageWindow<double> leaning_position_window_{3'600'000'000'000L};
  std::unique_ptr<CsvWriter> price_writer_;
};

class RmmPricer {
 public:
  explicit RmmPricer(
      RmmSubStrategy* parent,
      const IProduct* product,
      IProductHolder*,
      std::string mea_string,
      bool is_sim);
  bool onBookFeed(const FeedUpdate& upd);
  void onTradeFeed(const FeedUpdate& upd, bool reduce_only);
  void SetBookTimestamp(int64_t timestamp) { book_ts_ = timestamp; }
  void SetTradeCumSellQty(double qty) { trade_cum_sell_qty_ = qty; }
  void SetTradeCumBuyQty(double qty) { trade_cum_buy_qty_ = qty; }
  void SetTradeTimestamp(int64_t timestamp) { trade_ts_ = timestamp; }
  void SetPriceOffset(int64_t price_offset) { price_offset_ = price_offset; }
  void SetReady(bool ready) { ready_ = ready; }
  void SetFairPrice(double fair_price) { fair_price_ = fair_price; }
  template <typename T1, typename T2>
  void UpdateSnapshotFeed(const T1& feed_orders, T2* side_orders, int32_t sign);
  void UpdateRefOrders(const FeedUpdate& upd, int64_t timestamp);
  bool Ready() { return ready_; }
  bool SpotRef() { return spot_ref_; }
  bool BookUpdated() { return book_updated_; }
  int64_t PriceOffset() { return price_offset_; }
  int64_t BookTimestamp() {return book_ts_; }
  int64_t TradeTimestamp() { return trade_ts_; }
  int64_t IndexTimestamp() { return fair_price_window_.ref_index_timestamp; }
  double IndexPrice() { return fair_price_window_.ref_indexp; }
  double FairPrice() { return fair_price_; }
  double Midp() { return 0.5 * (ask0p_ + bid0p_); }
  double SnapshotMidp() { return snapshot_midp_; }
  double Multiplier() { return multiplier_; }
  double TradeCumSellQty() { return trade_cum_sell_qty_; }
  double TradeCumBuyQty() { return trade_cum_buy_qty_; }
  double RefWeight() { return ref_weight_; }
  const std::map<int64_t, double>& AskOrders() { return ask_orders_; }
  const std::map<int64_t, double, std::greater<int64_t> >& BidOrders() { return bid_orders_; }
  MarketExchangeApi Mea() { return mea_; }
  RmmPricerWindow* FairPriceWindow() { return &fair_price_window_; }
  TimeSampledMovingAverage* FairPriceOffsetWindow() { return &fair_price_offset_window_; }
  double NameMultiplier() { return *name_multiplier_; }
  std::string QuoteString() { return quote_str_; }

 private:
  bool ready_{false};
  bool spot_ref_;
  bool book_updated_{false};
  bool is_sim_{false};
  int64_t price_offset_{0L};
  int64_t book_ts_{0L};
  int64_t book_reset_ts_{0L};
  int64_t trade_ts_{0L};
  double tick_size_{0L};
  double ask0p_{0};
  double bid0p_{0};
  double ask0q_{0};
  double bid0q_{0};
  double snapshot_midp_{0};
  double trade_cum_sell_qty_{0};
  double trade_cum_buy_qty_{0};
  double multiplier_{0};
  double fair_price_{0};
  double ref_weight_{1.0};
  std::optional<double> name_multiplier_;
  std::map<int64_t, double> ask_orders_;
  std::map<int64_t, double, std::greater<int64_t> > bid_orders_;
  std::string quote_str_;
  std::unique_ptr<IProductHolder> ref_product_holder_;
  MarketExchangeApi mea_;
  RmmSubStrategy* parent_{nullptr};
  RmmPricerWindow fair_price_window_;
  presto::math::MovingAverageWindow<double> spread_window_;
  TimeSampledMovingAverage fair_price_offset_window_;
};

}  // namespace coin2::strategy::rmm_strat
